/*
  Copyright 2006 by Daniel Kuebrich
  Licensed under the Academic Free License version 3.0
  See the file "LICENSE" for more information
*/

// Class RuleUI
package sim.app.lsystem;
import java.io.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.table.*;
import sim.util.gui.*;

// This class becomes the Rules pane of the Console

public class RuleUI extends JPanel
    {
    private static final long serialVersionUID = -8650952120231540392L;
    // components
    JButton buttonGo = new JButton("Calculate");
    JButton buttonCancel = new JButton("Cancel");
    JButton buttonSave = new JButton("Save");
    JButton buttonLoad = new JButton("Load");
    JButton buttonHelp = new JButton("Help");

    // put the table in its own scroll pane, as the console is not too big
    JTable ruleTable = new JTable(20,2);
    JScrollPane scrollPane = new JScrollPane(ruleTable);
    
    JProgressBar calcProgress = new JProgressBar(0,100);
    
    JTextField seedField = new JTextField("F-F-F-F", 10);
    JTextField stepField = new JTextField("3", 3);
    
    // help panel (see very end of the init() function for elaboration on this)
    JPanel helpPanel = new JPanel();
    
    // references to sim with ui, sim state
    LSystemWithUI lsui;
    LSystem ls;
    
    // so that we can update the draw settings tab also
    DrawUI dui;
    
    // for calculation thread
    int steps=0;                // expansions
    Runnable calcRunnable;
    Thread calcThread;
    Object lock = new Object();
    boolean stop = false;
    
    // returns the frame that should be used as the parent frame for dialogs
    public Frame getFrame()
        {
        Component c = this;
        
        while(c.getParent() != null)
            c = c.getParent();
        
        return (Frame)c;
        }
    
    
    // this is currently used only by buttonGo
    // it takes the currently entered data for rules and sends it to the LSystem.LSystemData intstance
    void getRulesFromUI()
        {
        // set l-system parameters
            
        // seed
        ls.l.seed = seedField.getText();
        LSystemData.setVector(ls.l.code, ls.l.seed);
        // expansions
        ls.l.expansions = Integer.valueOf(stepField.getText()).intValue();
        
        // erase old rules
        ls.l.rules.clear();
        
        // build new rule list
        for(int r=0; r<ruleTable.getRowCount(); r++)
            {
            // if both not null
            if(ruleTable.getValueAt(r,0) != null && ruleTable.getValueAt(r,1) != null)
                // and both not zero length
                if(((String)(ruleTable.getValueAt(r,0))).length() > 0 && ((String)(ruleTable.getValueAt(r,0))).length() > 0)
                    ls.l.rules.add( new Rule( (byte)(((String)(ruleTable.getValueAt(r,0))).
                                substring(0,1).charAt(0)), (String)ruleTable.
                            getValueAt(r,1)) );
            }
        
        // set # of expansions
        steps = Integer.valueOf(stepField.getText()).intValue();
        }
    
    
    // constructor
    public RuleUI(LSystemWithUI nLsui, DrawUI nDui)
        {
        lsui = nLsui;
        ls = (LSystem)lsui.state;
        dui = nDui;
        
        try
            {
            init();
            }
        catch (Exception e)
            {
            e.printStackTrace();
            }
        }
    
    
    
    public void init()
        {
        // This runnable calculates the expansions of the L-system when the "Calculate"
        // button is pushed.  Because it runs in a separate thread, it can conveniently 
        // be cancelled, and updates a JProgressBar to show that it is still thinking.
        calcRunnable = new Runnable()
            {
            public void run()
                {
                int h=0; //number of expansions
                int p=0; //position in original code
                int r=0; //rule check index
                boolean ruleApplied = false; // has a rule been applied to this symbol yet
                
                // Speed... Make a new ByteList and copy into there
                // instead of inserting into the old one and shifting the elements over...
                // Also, I have not written an insert function, so this is a double bonus.
                ByteList newCode;
                newCode = new ByteList(ls.l.code.b.length);
                
                // main expanion loop
                while(true)
                    {
                    // stop if external stop requested
                    // this occurrs when the cancel button is pressed
                    synchronized(lock)
                        {
                        if(stop)
                            break;
                        }
                    
                    // stop if enough expansions have been completed
                    if(h >= steps)
                        break;
                    
                    // else keep expanding
                    ruleApplied = false;        // reset this
                    
                    for(r=0; r<ls.l.rules.size(); r++)
                        {
                        if(ls.l.code.b[p] == (((Rule)ls.l.rules.get(r)).pattern))               // replace!
                            {
                            newCode.addAll(((Rule)ls.l.rules.get(r)).replace);
                            ruleApplied = true;
                            
                            // dont try to expand extra rules, that would be trouble
                            break;
                            }
                        }
                    
                    if(!ruleApplied)            // if no rule was found for this item
                        {
                        newCode.add(ls.l.code.b[p]);
                        }
                    
                    p++;                        // increment p to go to the next
                    
                        
                    // Cycle the progress bar to show thinking!
                    // You're probably thinking.. "Hey.. this is such a waste of time, why 
                    // didn't you just use that nifty 'barber pole' auto-scrolling effect?"
                    // Well, that's good stuff, and it's called "Indeterminate mode".  However, 
                    // this capability seems to only exist in >= 1.4...
                    if(p%100 == 0)
                        {
                        SwingUtilities.invokeLater(
                            new Runnable()
                                {
                                public void run()
                                    {
                                    int i = calcProgress.getValue();
                                    if(i < 100)
                                        i++;
                                    else
                                        i = 0;
                                    
                                    calcProgress.setValue(i);
                                    }
                                }
                            );
                        }
                     
                    // an expansion has been completed
                    // hurray
                    if(p >= ls.l.code.length)
                        {
                        p = 0;
                        h++;
                        ls.l.code = newCode;
                        newCode = new ByteList(ls.l.code.length);
                        }
                    }
                // end main expansion loop

                // on successful end, enable calculate and disable cancel buttons
                SwingUtilities.invokeLater(
                    new Runnable()
                        {
                        public void run()
                            {
                            buttonGo.setEnabled(true);
                            buttonCancel.setEnabled(false);
                            calcProgress.setValue(0);
                            calcProgress.setString("Done!");
                            }
                        }
                    );
            
                }// end run
            };
        
        // buttonGo calculates the expansions of the rules from the given seed
        buttonGo.addActionListener(new ActionListener()
            {
            public void actionPerformed(ActionEvent e)
                {
                getRulesFromUI();
                
                // now we will begin..
                calcProgress.setString("Calculating...");
                
                // expand
                // in a separate thread
                stop = false;
                calcThread = new Thread(calcRunnable);
                calcThread.start();
                
                // juggle buttons
                buttonCancel.setEnabled(true);
                buttonGo.setEnabled(false);
                }
            });
                
        // buttonCancel stops an expansion being processed (started by buttonGo "Calculate")
        buttonCancel.addActionListener(new ActionListener()
            {
            public void actionPerformed(ActionEvent e)
                {
                synchronized(lock)
                    {
                    // let the thread stop on its own...
                    stop = true;
                    }
                
                // but wait for it
                try
                    {
                    calcThread.join();
                    }
                catch (Exception ex)
                    {
                    ex.printStackTrace();
                    }
                
                // reset buttons
                calcProgress.setValue(0);
                calcProgress.setString("Cancelled");
                buttonCancel.setEnabled(false);
                buttonGo.setEnabled(true);
                }
            });
        
        
        // buttonSave saves the current seed, rules, draw settings, and expansions
        // Saves the data after a Calculate has been executed.. so if you
        // Enter data A
        // then Calculate
        // then Enter data B
        // then save, you will be saving data A.
        // so be careful!
        buttonSave.addActionListener(new ActionListener()
            {
            public void actionPerformed(ActionEvent e)
                {
                try
                    {

                    JOptionPane.showMessageDialog( getFrame(), 
                        "<html>IF you have changed the settings since the last time you calculated the L-system,<br>"+
                        "the L-system you save will be the one last calculated--not the current data!</html>" );
                    
                    // show save dialog
                    FileDialog fd = new FileDialog(getFrame(), 
                        "Save Current L-System Settings As", FileDialog.SAVE);
                    fd.setFile("Untitled.lss");
                    fd.setVisible(true);;
                    
                    // on cancel, return
                    if(fd.getFile() == null)
                        return;
                    
                    // else do the thing
                    File outputFile = new File(fd.getDirectory(), fd.getFile());
                    FileOutputStream outputFileStream = new FileOutputStream(outputFile);
                    java.util.zip.GZIPOutputStream g = new java.util.zip.GZIPOutputStream(
                        new BufferedOutputStream(outputFileStream));
                    
                    ObjectOutputStream out = new ObjectOutputStream(g);
                    out.writeObject(ls.l);
                    
                    // now need to do a little dance with the GZIPOutputStream to write
                    // this stuff out correctly -- see sim.engine.SimState.writeToCheckpoint(OutputStream)
                    // for more info
                    
                    out.flush();
                    g.finish();
                    g.flush();
                    out.close();
                    }
                catch (Exception ex)
                    {
                    ex.printStackTrace();
                    }
                }
            });
        
        // buttonLoad loads the file's seed, rule, and expansions
        buttonLoad.addActionListener(new ActionListener()
            {
            public void actionPerformed(ActionEvent e)
                {
                try
                    {
                    // show load dialog
                    FileDialog fd = new FileDialog(getFrame(), 
                        "Open L-System Settings File (.lss)", FileDialog.LOAD);
                    fd.setFile("*.lss");
                    fd.setVisible(true);;
                    
                    // on cancel, return
                    if(fd.getFile() == null)
                        return;
                        
                    File inputFile = new File(fd.getDirectory(), fd.getFile());
                    FileInputStream inputFileStream = new FileInputStream(inputFile);
                    ObjectInputStream in = new ObjectInputStream(
                        new java.util.zip.GZIPInputStream(new BufferedInputStream(inputFileStream)));
                    ls.l = (LSystemData)in.readObject();
                    in.close();
                    
                    
                    // change UI fields to reflect newly loaded settings
                    // seed
                    seedField.setText(ls.l.seed);
                    // # expansions
                    stepField.setText(String.valueOf(ls.l.expansions));
                    // line size
                    dui.distField.setText(String.valueOf(ls.l.segsize));
                    // angle... has been stored in radians, so un-radian it
                    dui.angleField.setText(String.valueOf(ls.l.angle*180/Math.PI));
                    // x, y
                    // --- unneccessary now that Display2D options do this
                    //dui.xField.setText(String.valueOf(ls.l.x));
                    //dui.yField.setText(String.valueOf(ls.l.y));
                    
                    
                    // rules
                    // first clear table
                    for(int t=0; t<ruleTable.getRowCount(); t++)
                        {
                        ruleTable.setValueAt(null, t, 0);
                        ruleTable.setValueAt(null, t, 1);
                        }
                    
                    // now set new stuff
                    for(int t=0; t<ls.l.rules.size(); t++)
                        {
                        ruleTable.setValueAt(String.valueOf((char)((Rule)(ls.l.rules.get(t))).pattern), t, 0);
                        ruleTable.setValueAt(LSystemData.fromVector(((Rule)(ls.l.rules.get(t))).replace), t, 1);
                        }
                    }
                catch (Exception ex)
                    {
                    ex.printStackTrace();
                    }
                }
            });
        
        // buttonHelp displays symbol help
        buttonHelp.addActionListener(new ActionListener()
            {
            public void actionPerformed(ActionEvent e)
                {
                // this makes it free-floating rather than modal
                // so you can move to the side and consult the dialog while using the console
                JFrame help = new JFrame();
                help.getContentPane().setLayout( new BorderLayout() );
                help.getContentPane().add(helpPanel, BorderLayout.CENTER);
                help.setSize(400,300);
                help.setVisible(true);
                }
            });

         
        ///// OK
        // now build the actual UI
        this.setLayout(new BorderLayout());
        
        // so much panel juggling
        // looks like this:
        /*
          All JPanels use BorderLayout.
          N = NORTH, C = CENTER, S = SOUTH
          
          +-------JPanel this (ruleUI)-----------------+
          | +-------JPanel "mid"---------------------+ |
          | | +--------JPanel "top"----------------+ | |
          | | | +--------LabelledList "list"-----+ | | |
          | | |N| Seed           =============== | | | |
          | |N| | Expanions      =============== | | | |
          |N| | +--------------------------------+ | | |
          | | |C  Box( Calculate   Cancel )        | | |
          | | |S  (____progressbar__________)      | | |
          | | +------------------------------------+ | |
          | |C  Box( Save    Load )                  | |
          | +----------------------------------------+ |
          |                                            |
          |C     JScrollPane(JTable rules)             |
          +--------------------------------------------+
        
          So that's the overview of the next 50 lines or so
        */
        
        JPanel top = new JPanel();
        JPanel mid = new JPanel();
        top.setLayout(new BorderLayout());
        mid.setLayout(new BorderLayout());
        
        // List of L-system parameters.. what is a LabelledList?
        // sim.display.LabelledList is a convenient way to draw lists of the format
        //  text   component
        //  text   component
        LabelledList list = new LabelledList()
            {
            private static final long serialVersionUID = -2153709747861890863L;
            Insets insets = new Insets(5, 5, 5, 5);
            public Insets getInsets()
                {
                return insets;
                }
            };
        
        seedField.setText(ls.l.seed);
        
        list.addLabelled("Seed", seedField);
        list.addLabelled("Expansions", stepField);
        
        // add everything so far (in list)
        top.add(list, BorderLayout.NORTH);
        
        // box with calculate and cancel buttons
        Box b = new Box(BoxLayout.X_AXIS)
            {
            private static final long serialVersionUID = -869949739122977643L;
            Insets insets = new Insets(5, 5, 5, 5);
            public Insets getInsets()
                {
                return insets;
                }
            };
        
        b.add(buttonGo);
        b.add(buttonCancel);
        buttonCancel.setEnabled(false);
        b.add(Box.createGlue());     
        
        top.add(b, BorderLayout.CENTER);
        
        top.add(calcProgress, BorderLayout.SOUTH);
        calcProgress.setStringPainted(true);
        calcProgress.setString("Press Calculate");
        
        b = new Box(BoxLayout.X_AXIS)
            {
            private static final long serialVersionUID = -2124038237393174259L;
            Insets insets = new Insets(5, 5, 5, 5);
            public Insets getInsets()
                {
                return insets;
                }
            };
        
        b.add(buttonSave);
        b.add(buttonLoad);
        b.add(buttonHelp);
        b.add(Box.createGlue());     
        
        mid.add(top, BorderLayout.NORTH);
        mid.add(b, BorderLayout.CENTER);
        this.add(mid, BorderLayout.NORTH);
        
        // allows table to have named headers
        class NamedTableModel extends DefaultTableModel
            {
            private static final long serialVersionUID = -6638838065039609876L;

            NamedTableModel(int rows, int cols)
                {
                super(rows, cols);
                }
            
            public String getColumnName( int i )
                {
                if(i == 0)
                    return "Symbol";
                else if(i == 1)
                    return "Replacement";
                else
                    return "Error.";
                }
            };
        
        ruleTable.setModel(new NamedTableModel(20,2));
        
        // rules table setup
        // same defaults as in LSystemWithUI.java
        seedField.setText("F");
        
        ruleTable.setValueAt("F", 0, 0);
        ruleTable.setValueAt("F[+F]F[-F]F", 0, 1);
        
        this.add(scrollPane, BorderLayout.CENTER);
        
        
        // Make the Help popup!
        // This is executed on a click of buttonHelp.. but we've got it set up now.
        LabelledList list2 = new LabelledList();
        
        helpPanel.setLayout(new BorderLayout());
        
        list2.addLabelled("Symbols", new JLabel(""));
        list2.addLabelled("Uppercase (A-Z)", new JLabel("Draw forward Distance units"));
        list2.addLabelled("Lowercase (a-z)", new JLabel("Move forward Distance units (no draw)"));
        list2.addLabelled("-", new JLabel("Turn right by angle degrees"));
        list2.addLabelled("+", new JLabel("Turn left by angle degrees"));
        list2.addLabelled("[", new JLabel("Push position, angle"));
        list2.addLabelled("]", new JLabel("Pop position, angle"));
        list2.addLabelled("", new JLabel(""));
        list2.addLabelled("Save: ", new JLabel("Saves the rules, seed, draw settings, and "));
        list2.addLabelled("", new JLabel("calculated expansions of the "));
        list2.addLabelled("", new JLabel("Last calculated L-system."));
        list2.addLabelled("Load: ", new JLabel("Loads saved L-system settings."));
        
        helpPanel.add(list2, BorderLayout.CENTER);
        }
    }
